package tms.model;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Calendar;
import java.util.Map;
import java.util.TreeMap;

/**
 * The DependencyManager class is a singleton representing the dependencies
 * between tasks in the TMS.
 * It also resolves duration and deadline conflicts between dependent tasks.
 * It extends AbstractModelObject and implements PropertyChangeListener
 * to allow the fire of property changes
 * 
 * @author Rolf Schmidt
 * @version 1.0
 * 
 */
public class DependencyManager extends AbstractModelObject implements PropertyChangeListener {

	/**
	 * the only instance of the class
	 */
	private static DependencyManager s_instance = null;

	/**
	 * holds all Dependency objects (Task.Id, Task.Id)
	 */
	private TreeMap<Integer, Integer> dependencyMap = new TreeMap<Integer, Integer>();

	/**
	 * getter of the only instance of the class
	 * 
	 * @return the only instance of the class
	 */
	public static DependencyManager getInstance() {
		if (s_instance == null)
			s_instance = new DependencyManager();
		return s_instance;
	}

	/**
	 * private constructor to prevent the creation the multiple instances of the
	 * class
	 */
	private DependencyManager() {
	}

	/**
	 * @return the dependencyMap
	 */
	public TreeMap<Integer, Integer> getDependencyMap() {
		return dependencyMap;
	}

	/**
	 * Add a dependency to the dependency map
	 * A dependencyMap change property event will be fired if the addition of
	 * the dependency
	 * is successful
	 * 
	 * @param first
	 *            the first task to add (i.e. chronologically first)
	 * @param second
	 *            the second task to add (i.e. chronologically second)
	 */
	public void addDependency(Task first, Task second) {
		if (first != null && second != null) {
			if (first != second) {
				dependencyMap.put(first.getId(), second.getId());
				first.addPropertyChangeListener(this);
				second.addPropertyChangeListener(this);
				firePropertyChange("dependencyMap", null, this.dependencyMap);
			}
		}
	}

	/**
	 * remove a task from the taskList
	 * If the task to be removed is null or does not exist, then it will not be
	 * removed
	 * A tasks change property event will be fired if the removal of the task is
	 * successful
	 * 
	 * @param task
	 *            the task to remove
	 */
	public void removeDependency(Task first) {
		if (first != null) {
			dependencyMap.remove(first.getId());
			firePropertyChange("dependencyMap", null, this.dependencyMap);
		}
	}

	/**
	 * clear the dependencyMap
	 */
	public void clear() {
		dependencyMap.clear();
		firePropertyChange("dependencyMap", null, this.dependencyMap);
	}

	/**
	 * resolves dependencies between tasks (overlaps due to duration, deadline;
	 * circular dependencies)
	 */
	private void resolveDependencies(Task task, int oldBeforeId) {
		TaskTree taskTree = TaskTree.getInstance();
		
		if (task != null) {
			// delete the dependency if the new beforeId is -1
			if (task.getBeforeId() < 0) {
				removeDependency(task);
			} else {
				Task beforeTask = taskTree.getTask(task.getBeforeId());
				// does the new before task exist?
				if (beforeTask != null) {
					removeDependency(task);
					addDependency(task, beforeTask);
					// if the new dependency introduces a circular reference
					// we undo the changes
					if (isCircular(task)) {
						removeDependency(task);
						if (oldBeforeId >= 0) {
							Task oldBeforeTask = taskTree.getTask(oldBeforeId);
							addDependency(task, oldBeforeTask);
						}
						task.setBeforeId(oldBeforeId);
					} else {
						resolveOverlaps();
					}
				} else {
					task.setBeforeId(oldBeforeId);
				}
			}
		}
	}

	/**
	 * tests if task is involved in a circular dependency
	 */
	private boolean isCircular(Task task) {
		TaskTree taskTree = TaskTree.getInstance();
		
		if (task != null) {
			Task circularTest = task;
			int beforeId = task.getBeforeId();
			// check the chain of beforeIds to find out if we can end up with
			// the same task that we start with
			while (beforeId >= 0) {
				// get the 'before' task
				circularTest = taskTree.getTask(beforeId);
				// if the 'before' task and the task are identical, we have a
				// circular dependency
				if (circularTest == task) {
					return true;
				}
				beforeId = circularTest.getBeforeId();
			}
		}
		return false;
	}

	/**
	 * resolves temporal overlaps after changing durations or deadlines
	 * of dependent tasks
	 */
	private void resolveOverlaps() {
		TaskTree taskTree = TaskTree.getInstance();

		boolean resolvedOberlaps = false;

		// iterate over all existing dependencies
		for (Map.Entry<Integer, Integer> entry : getDependencyMap().entrySet()) {
			Task first = taskTree.getTask(entry.getKey());
			Task second = taskTree.getTask(entry.getValue());

			// the deadline of the first task
			Calendar endDateFirst = Calendar.getInstance();
			endDateFirst.setTime(first.getDeadline());

			// the deadline of the second task
			Calendar endDateSecond = Calendar.getInstance();
			endDateSecond.setTime(second.getDeadline());

			// the start date of the second task
			// i.e. deadline minus duration
			Calendar startDateSecond = Calendar.getInstance();
			startDateSecond.setTime(second.getDeadline());
			startDateSecond.add(Calendar.DAY_OF_MONTH, (-1) * second.getDuration());
			// if the deadline of the first task overlaps with
			// the start date of the second task, the deadline of the second
			// task will be pushed back
			if (startDateSecond.before(endDateFirst)) {
				resolvedOberlaps = true;
				endDateSecond.setTime(endDateFirst.getTime());
				endDateSecond.add(Calendar.DAY_OF_MONTH, second.getDuration());
				second.setDeadline(endDateSecond.getTime());
			}
		}
		// only trigger the property change if at least one overlap was resolved
		if (resolvedOberlaps) {
			firePropertyChange("dependencyMap", null, this.dependencyMap);
		}
	}

	@Override
	public void propertyChange(PropertyChangeEvent evt) {
		String propertyName = evt.getPropertyName();

		// resolve time conflicts for dependent tasks
		if (propertyName.equals("duration")) {
			resolveOverlaps();
		}
		if (propertyName.equals("deadline")) {
			resolveOverlaps();
		}
		if (propertyName.equals("beforeId")) {

			if (evt.getSource() instanceof Task) {
				Task task = (Task) evt.getSource();

				int oldBeforeId = (Integer) evt.getOldValue();
				resolveDependencies(task, oldBeforeId);
			}
		}
	}

}
